/*
 * fv3tk
 * Copyright (C) 2009  John Pritchard, jdp@syntelos.org
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package fv3tk;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

import java.util.Enumeration;
import java.util.Properties;

/**
 * The HID thread drives the reading of devices into their current
 * state objects.
 */
public final class Fv3Hid 
    extends java.lang.Thread
{
    public final static String PropnPrefix = "fv3tk.Fv3Hid.Device";

    /**
     * Number of nano seconds in a millisecond.
     */
    public final static long Nano = (long)1e6;

    /**
     * A device is a list of events in a device class.  See {@link
     * fv3tk.hid.Button} and {@link fv3tk.hid.Pad} and {@link
     * fv3tk.hid.Stick}.
     * 
     * <h3>Init</h3>
     * 
     * A device class has a public static method named
     * <code>"Init"</code>.  The init method requires argument {@link
     * Fv3Hid}.
     * 
     * <h3>Run</h3>
     * 
     * A device class has a public static method named
     * <code>"Run"</code>.  The run method requires a long number of
     * nanoseconds in which the method should complete and return a
     * member of its (Device) class (or null).
     * 
     * The long argument is guaranteed to be greater than or equal to
     * one nanosecond.  Implementors should guarantee the performance
     * of a normal return of less than or equal to the argument.
     * Implementors should design for a return time margin of error
     * far less than millisecond (error &lt;&lt; 1 ms).
     * 
     * <i>Note: nanoseconds 1e-9 (seconds); milliseconds 1e-3 (seconds)</i>.
     * 
     * <h3>Shutdown</h3>
     * 
     * A device class has a public static method named
     * <code>"Shutdown"</code>.  Shutdown takes no arguments and
     * returns Void.  This function is called to completely tear down
     * and dispose of the device's live resources.
     * 
     * @see fv3tk.hid.Device
     */
    public interface Device 
        extends java.lang.Iterable<Device>, java.util.Iterator<Device>
    {

        public boolean alive();
        /**
         * The event type is defined by the {@link
         * fv3tk.hid.InputEvent} interfaces that are implemented by
         * instances of this class.
         */
        public int getEventType();

        public boolean isConsumed();

        public void consume();

        public Device insert(Device list);

        public boolean hasNext();

        public Device next();

        public void destroy();
    }

    /**
     * Each member of the HID device table is a device class operator
     * for configure, init and run.
     * 
     * The {@link Fv3Hid} uses this to drive a {@link Device}.
     * 
     */
    protected static class DeviceTable {

        private final static Object Static = null;
        private final static Object[] NoArgs = {};

        protected final static String MethodNameInit = "Init";
        private final static Class[] MethodParamsInit = {
            fv3tk.Fv3Hid.class
        };
        protected final static String MethodNameRun = "Run";
        private final static Class[] MethodParamsRun = {
            java.lang.Long.TYPE
        };
        protected final static String MethodNameShutdown = "Shutdown";
        private final static Class[] MethodParamsShutdown = {
        };


        protected final Class<Fv3Hid.Device> deviceClass;

        protected final Method deviceInit;

        protected final Method deviceRun;

        protected final Method deviceShutdown;

        protected volatile Device list;

        private volatile boolean updating, running;

        /**
         * Configure
         */
        DeviceTable(Class<Fv3Hid.Device> deviceClass)
            throws IllegalArgumentException
        {
            super();
            if (null != deviceClass){
                this.deviceClass = deviceClass;
                try {
                    this.deviceInit = deviceClass.getMethod(MethodNameInit,MethodParamsInit);
                    if (!Modifier.isStatic(this.deviceInit.getModifiers())){
                        String erm = String.format("Method '%s(%s)' in class '%s' is not static.",MethodNameInit,"Fv3Hid",deviceClass.getName());
                        throw new IllegalArgumentException(erm);
                    }
                }
                catch (Exception exc){
                    String erm = String.format("Missing method in class '%s', name '%s' arguments (%s).",deviceClass.getName(),MethodNameInit,"Fv3Hid");
                    throw new IllegalArgumentException(erm,exc);
                }
                try {
                    this.deviceRun = deviceClass.getMethod(MethodNameRun,MethodParamsRun);
                    if (!Modifier.isStatic(this.deviceRun.getModifiers())){
                        String erm = String.format("Method '%s(%s)' in class '%s' is not static.",MethodNameRun,"long",deviceClass.getName());
                        throw new IllegalArgumentException(erm);
                    }
                }
                catch (Exception exc){
                    String erm = String.format("Missing method in class '%s', name '%s' arguments (%s).",deviceClass.getName(),MethodNameRun,"long");
                    throw new IllegalArgumentException(erm,exc);
                }
                try {
                    this.deviceShutdown = deviceClass.getMethod(MethodNameShutdown,MethodParamsShutdown);
                    if (!Modifier.isStatic(this.deviceShutdown.getModifiers())){
                        String erm = String.format("Method '%s(%s)' in class '%s' is not static.",MethodNameShutdown,"",deviceClass.getName());
                        throw new IllegalArgumentException(erm);
                    }
                }
                catch (Exception exc){
                    String erm = String.format("Missing method in class '%s', name '%s' arguments (%s).",deviceClass.getName(),MethodNameShutdown,"");
                    throw new IllegalArgumentException(erm,exc);
                }
            }
            else
                throw new IllegalArgumentException();
        }

        protected boolean alive(){
            return (null != this.list && this.list.alive());
        }
        /**
         * Init
         */
        protected void init(Fv3Hid hid){
            try {
                this.deviceInit.invoke(Static,hid);
            }
            catch (IllegalAccessException exc){
                String erm = String.format("%s: error invoking %s.%s(hid)",Thread.currentThread().getName(),this.deviceClass.getName(),this.deviceInit.getName());
                throw new IllegalStateException(erm,exc);
            }
            catch (InvocationTargetException exc){
                String erm = String.format("%s: error invoking %s.%s(hid)",Thread.currentThread().getName(),this.deviceClass.getName(),this.deviceInit.getName());
                throw new IllegalStateException(erm,exc);
            }
        }
        /**
         * Run
         */
        protected void run(long nanos){
            this.running = true;
            Device list, temp;
            try {
                temp = (Device)this.deviceRun.invoke(Static,nanos);
                if (null != temp){
                    list = this.list;
                    if (null != list)
                        this.list = list.insert(temp);
                    else
                        this.list = temp;
                }
            }
            catch (ClassCastException exc){
                String erm = String.format("%s: error invoking %s.%s(long)",Thread.currentThread().getName(),this.deviceClass.getName(),this.deviceRun.getName());
                throw new IllegalStateException(erm,exc);
            }
            catch (IllegalAccessException exc){
                String erm = String.format("%s: error invoking %s.%s(long)",Thread.currentThread().getName(),this.deviceClass.getName(),this.deviceRun.getName());
                throw new IllegalStateException(erm,exc);
            }
            catch (InvocationTargetException exc){
                String erm = String.format("%s: error invoking %s.%s(long)",Thread.currentThread().getName(),this.deviceClass.getName(),this.deviceRun.getName());
                throw new IllegalStateException(erm,exc);
            }
            finally {
                this.running = false;
            }
        }
        protected void update(Fv3Component fv3c){
            this.updating = true;
            Device list;
            try {
                list = this.list;
                this.list = null;
                if (null != list)
                    fv3c.input(list);
            }
            finally {
                this.updating = false;
            }
        }
        protected void destroy(){
            try {
                this.deviceShutdown.invoke(Static,NoArgs);
            }
            catch (IllegalAccessException exc){
                String erm = String.format("%s: error invoking %s.%s(hid)",Thread.currentThread().getName(),this.deviceClass.getName(),this.deviceShutdown.getName());
                throw new IllegalStateException(erm,exc);
            }
            catch (InvocationTargetException exc){
                String erm = String.format("%s: error invoking %s.%s(hid)",Thread.currentThread().getName(),this.deviceClass.getName(),this.deviceShutdown.getName());
                throw new IllegalStateException(erm,exc);
            }
            Device list = this.list;
            if (null != list){
                this.list = null;
                list.destroy();
            }
        }
        public boolean equals(Object that){
            if (this == that)
                return true;
            else if (null == that)
                return false;
            else if (that instanceof DeviceTable)
                return this.deviceClass.equals( ((DeviceTable)that).deviceClass);
            else
                return false;
        }
    }


    protected volatile static Fv3Hid Instance;

    public final static Fv3Hid Current(){
            
        return Instance;
    }
    public final static void Add(Class<Fv3Hid.Device> deviceclass){

        Instance.add(deviceclass);
    }


    protected final Class<Fv3Hid.Device> deviceClassSuper;

    protected final Fv3Component fv3c;

    protected volatile DeviceTable[] list;


    protected Fv3Hid(Fv3Component fv3c){
        super("FV3 HID");

        this.deviceClassSuper = Fv3Hid.Device.class;

        this.fv3c = fv3c;

        this.setDaemon(true);
        this.setPriority(MIN_PRIORITY+1);
        {
            Properties sys = java.lang.System.getProperties();
            Enumeration<String> names = (Enumeration<String>)sys.propertyNames();
            while (names.hasMoreElements()){
                String name = names.nextElement();
                if (name.startsWith(PropnPrefix)){
                    String value = sys.getProperty(name);
                    try {
                        Class dc = Class.forName(value);
                        if (this.deviceClassSuper.isAssignableFrom(dc))
                            this.add( (Class<Fv3Hid.Device>) dc);
                        else {
                            String erm = String.format("%s: error in configured '%s' classname '%s' not a member of 'fv3tk.Fv3Hid.Device'.",Thread.currentThread().getName(),name,value);
                            System.err.println(erm);
                        }
                    }
                    catch (ClassNotFoundException exc){
                        String erm = String.format("%s: class not found for configured '%s' classname '%s'.",Thread.currentThread().getName(),name,value);
                        System.err.println(erm);
                    }
                }
            }
        }
        Instance = this;
    }


    protected void input(){

        DeviceTable[] list = this.list;
        if (null != list){

            Fv3Component fv3c = this.fv3c;

            int count = list.length;
            for (int cc = 0; cc < count; cc++){
                list[cc].update(fv3c);
            }
        }
    }

    protected void hidDestroy(){

        DeviceTable[] list = this.list;
        if (null != list){
            this.list = null;

            int count = list.length;
            for (int cc = 0; cc < count; cc++){
                list[cc].destroy();
            }
        }
    }

    public void add(Class<Fv3Hid.Device> deviceclass){

        DeviceTable device = new DeviceTable(deviceclass);

        DeviceTable[] list = this.list;
        if (null == list)
            this.list = new DeviceTable[]{device};
        else {
            for (DeviceTable unique : list){
                if (unique.equals(device))
                    return;
            }
            int len = list.length;
            DeviceTable[] copier = new DeviceTable[len+1];
            System.arraycopy(list,0,copier,0,len);
            copier[len] = device;
            this.list = copier;
        }

        device.init(this);

        synchronized(this){
            this.notifyAll();
        }
    }

    public void run(){
        while (true){
            long DT = Animator.Sync.DT * Nano;
            DeviceTable[] list = this.list;
            if (null != list){
                int count = list.length;
                long rt = (DT/count);
                if (Nano > rt)
                    rt = Nano;

                for (int cc = 0; cc < count; cc++){
                    list[cc].run(rt);
                }
                try {
                    synchronized(this){
                        this.wait(Animator.Sync.DT);
                    }
                }
                catch (InterruptedException exc){
                    return;
                }
            }
            else {
                try {
                    synchronized(this){
                        this.wait();
                    }
                }
                catch (InterruptedException exc){
                    return;
                }
            }
        }
    }

    synchronized void wake(){
        this.notifyAll();
    }
}
